package models

import (
	"crypto/md5"
	"errors"
	"fmt"
	"io"
	"strconv"
	"sync"
	"time"

	"github.com/astaxie/beego"
	"github.com/astaxie/beego/orm"
	_ "github.com/go-sql-driver/mysql"
)

var SessIonCreate *IdWorker

type Session struct {
	Id         int       `orm:"column(Id)"`
	OpenId     string    `orm:"column(OpenId)"`
	UnionId    string    `orm:"column(UnionId)"`
	Session    string    `orm:"column(Session)"`
	Createtime time.Time `orm:"column(CreateTime)"`
}

func (m *Session) TableName() string {
	return "Session"
}

func getSession(openId string, UnionId string) (string, error) {

	o := orm.NewOrm()
	o.Using("default")

	newSession := Session{
		OpenId:     openId,
		UnionId:    UnionId,
		Createtime: time.Now(),
		Session:    SessIonCreate.NextId(),
	}

	// 三个返回参数依次为：是否新创建的，对象 Id 值，错误
	if created, id, err := o.ReadOrCreate(&newSession, "OpenId"); err == nil {

		if created {
			beego.Debug("New Insert an object. Id:", id)

		} else {
			beego.Debug("Get an object. Id:", id)
		}
	} else {
		beego.Error(err)
		return "", err
	}

	if newSession.Createtime.Unix()-time.Now().Unix() > 24*60*60 {
		newSession.Session = SessIonCreate.NextId()
		newSession.Createtime = time.Now()
		_, err := o.Update(&newSession)
		if err != nil {
			beego.Error("update session failed!", err)
			return "", errors.New("登录失败，请稍后再试")
		}
	}

	return newSession.Session, nil
}

func GetOpenIdBySession(session string) (*Session, error) {

	o := orm.NewOrm()
	o.Using("default")

	s := Session{
		Session: session,
	}

	err := o.Read(&s, "Session")
	if err == orm.ErrNoRows {
		beego.Error("no session info ", session)
		return nil, err
	} else if err == orm.ErrMissPK {
		beego.Error("cannot find master key", session)
		return nil, err
	} else {
		return &s, nil
	}
}

const (
	CEpoch         = 1474802888000
	CWorkerIdBits  = 10 // Num of WorkerId Bits
	CSenquenceBits = 12 // Num of Sequence Bits

	CWorkerIdShift  = 12
	CTimeStampShift = 22

	CSequenceMask = 0xfff // equal as getSequenceMask()
	CMaxWorker    = 0x3ff // equal as getMaxWorkerId()
)

// IdWorker Struct
type IdWorker struct {
	workerId      int64
	lastTimeStamp int64
	sequence      int64
	maxWorkerId   int64
	lock          *sync.Mutex
}

// NewIdWorker Func: Generate NewIdWorker with Given workerid
func NewIdWorker(workerid int64) (iw *IdWorker, err error) {
	iw = new(IdWorker)

	iw.maxWorkerId = getMaxWorkerId()

	if workerid > iw.maxWorkerId || workerid < 0 {
		return nil, errors.New("worker not fit")
	}
	iw.workerId = workerid
	iw.lastTimeStamp = -1
	iw.sequence = 0
	iw.lock = new(sync.Mutex)
	return iw, nil
}

func getMaxWorkerId() int64 {
	return -1 ^ -1<<CWorkerIdBits
}

func getSequenceMask() int64 {
	return -1 ^ -1<<CSenquenceBits
}

// return in ms
func (iw *IdWorker) timeGen() int64 {
	return time.Now().UnixNano() / 1000 / 1000
}

func (iw *IdWorker) timeReGen(last int64) int64 {
	ts := time.Now().UnixNano()
	for {
		if ts < last {
			ts = iw.timeGen()
		} else {
			break
		}
	}
	return ts
}

// NewId Func: Generate next id
func (iw *IdWorker) NextId() string {
	iw.lock.Lock()
	defer iw.lock.Unlock()
	ts := iw.timeGen()
	if ts == iw.lastTimeStamp {
		iw.sequence = (iw.sequence + 1) & CSequenceMask
		if iw.sequence == 0 {
			ts = iw.timeReGen(ts)
		}
	} else {
		iw.sequence = 0
	}

	if ts < iw.lastTimeStamp {
		//err = errors.New("Clock moved backwards, Refuse gen id")
		//return 0, err
		ts = iw.lastTimeStamp + 1
	}

	iw.lastTimeStamp = ts
	ts = (ts-CEpoch)<<CTimeStampShift | iw.workerId<<CWorkerIdShift | iw.sequence

	sessionId := Md5(Md5(strconv.FormatInt(ts, 10)) + Md5(strconv.FormatInt(ts, 20)))

	return sessionId
}

func Md5(text string) string {

	hashMd5 := md5.New()
	io.WriteString(hashMd5, text)
	return fmt.Sprintf("%x", hashMd5.Sum(nil))
}

// ParseId Func: reverse uid to timestamp, workid, seq
func ParseId(id int64) (t time.Time, ts int64, workerId int64, seq int64) {
	seq = id & CSequenceMask
	workerId = (id >> CWorkerIdShift) & CMaxWorker
	ts = (id >> CTimeStampShift) + CEpoch
	t = time.Unix(ts/1000, (ts%1000)*1000000)
	return
}

func sessionInit() {
	SessIonCreate, _ = NewIdWorker(88)
}
